// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

import 'package:analysis_server/src/handler/legacy/completion_utils.dart';
import 'package:analysis_server/src/handler/legacy/legacy_handler.dart';
import 'package:analysis_server/src/legacy_analysis_server.dart';
import 'package:analysis_server/src/protocol_server.dart';
import 'package:analysis_server/src/provisional/completion/completion_core.dart';
import 'package:analysis_server/src/request_handler_mixin.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart';
import 'package:analysis_server/src/services/completion/dart/candidate_suggestion.dart';
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
import 'package:analysis_server/src/services/completion/yaml/analysis_options_generator.dart';
import 'package:analysis_server/src/services/completion/yaml/fix_data_generator.dart';
import 'package:analysis_server/src/services/completion/yaml/pubspec_generator.dart';
import 'package:analysis_server/src/services/completion/yaml/yaml_completion_generator.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/performance/operation_performance.dart';

/// The handler for the `completion.getSuggestions2` request.
class CompletionGetSuggestions2Handler extends CompletionHandler
    with RequestHandlerMixin<LegacyAnalysisServer> {
  /// Initialize a newly created handler to be able to service requests for the
  /// [server].
  CompletionGetSuggestions2Handler(
    super.server,
    super.request,
    super.cancellationToken,
    super.performance,
  );

  /// Computes completion results for [request] and append them to the stream.
  ///
  /// Clients should not call this method directly as it is
  /// automatically called when a client listens to the stream returned by
  /// 'results'. Subclasses should override this method, append at least one
  /// result to the controller, and close the controller stream once complete.
  Future<List<CandidateSuggestion>> computeFinalSuggestions({
    required CompletionBudget budget,
    required OperationPerformanceImpl performance,
    required DartCompletionRequest request,
    required int maxSuggestions,
    NotImportedSuggestions? notImportedSuggestions,
    required bool useFilter,
    required DartCompletionManager manager,
  }) async {
    //
    // Compute completions generated by server.
    //
    return await performance.runAsync('computeSuggestions', (
      performance,
    ) async {
      var collector = await manager.computeFinalizedCandidateSuggestions(
        request: request,
        performance: performance,
        maxSuggestions: maxSuggestions,
      );
      return collector.suggestions;
    });
  }

  /// Return the suggestions that should be presented in the YAML [file] at the
  /// given [offset].
  YamlCompletionResults computeYamlSuggestions(String file, int offset) {
    var provider = server.resourceProvider;
    var pathContext = provider.pathContext;
    if (file_paths.isAnalysisOptionsYaml(pathContext, file)) {
      var generator = AnalysisOptionsGenerator(provider);
      return generator.getSuggestions(file, offset);
    } else if (file_paths.isFixDataYaml(pathContext, file)) {
      var generator = FixDataGenerator(provider);
      return generator.getSuggestions(file, offset);
    } else if (file_paths.isPubspecYaml(pathContext, file)) {
      var generator = PubspecGenerator(provider, server.pubPackageService);
      return generator.getSuggestions(file, offset);
    }
    return const YamlCompletionResults.empty();
  }

  @override
  Future<void> handle() async {
    if (completionIsDisabled) {
      return;
    }

    var requestLatency = request.timeSinceRequest;
    var params = CompletionGetSuggestions2Params.fromRequest(
      request,
      clientUriConverter: server.uriConverter,
    );
    var file = params.file;
    var offset = params.offset;

    var timeoutMilliseconds = params.timeout;
    var budget = CompletionBudget(
      timeoutMilliseconds != null
          ? Duration(milliseconds: timeoutMilliseconds)
          : server.completionState.budgetDuration,
    );

    var provider = server.resourceProvider;
    var pathContext = provider.pathContext;

    if (file.endsWith('.yaml')) {
      var suggestions = computeYamlSuggestions(file, offset);
      server.sendResponse(
        CompletionGetSuggestions2Result(
          suggestions.replacementOffset,
          suggestions.replacementLength,
          suggestions.suggestions,
          false,
        ).toResponse(request.id, clientUriConverter: server.uriConverter),
      );
      return;
    }

    if (!file_paths.isDart(pathContext, file)) {
      server.sendResponse(
        CompletionGetSuggestions2Result(
          offset,
          0,
          [],
          false,
        ).toResponse(request.id, clientUriConverter: server.uriConverter),
      );
      return;
    }

    await performance.runAsync('request', (performance) async {
      var resolvedUnit = await performance.runAsync('resolveForCompletion', (
        performance,
      ) {
        return server.resolveForCompletion(
          path: file,
          offset: offset,
          performance: performance,
        );
      });
      if (resolvedUnit == null) {
        server.sendResponse(Response.fileNotAnalyzed(request, file));
        return;
      }

      if (offset < 0 || offset > resolvedUnit.content.length) {
        server.sendResponse(
          Response.invalidParameter(
            request,
            'params.offset',
            'Expected offset between 0 and source length inclusive,'
                ' but found $offset',
          ),
        );
        return;
      }

      var completionPerformance = CompletionPerformance(
        performance: performance,
        path: file,
        requestLatency: requestLatency,
        content: resolvedUnit.content,
        offset: offset,
      );
      server.recentPerformance.completion.add(completionPerformance);

      var analysisSession = resolvedUnit.analysisSession;

      var completionRequest = DartCompletionRequest(
        analysisSession: analysisSession,
        fileState: resolvedUnit.fileState,
        filePath: resolvedUnit.path,
        fileContent: resolvedUnit.content,
        libraryFragment: resolvedUnit.unitElement,
        offset: offset,
        unit: resolvedUnit.parsedUnit,
        dartdocDirectiveInfo: server.getDartdocDirectiveInfoForSession(
          analysisSession,
        ),
      );
      setNewRequest(completionRequest);

      var notImportedSuggestions = NotImportedSuggestions();
      var candidateSuggestions = <CandidateSuggestion>[];
      var suggestions = <CompletionSuggestion>[];
      var manager = DartCompletionManager(
        budget: budget,
        notImportedSuggestions: notImportedSuggestions,
      );
      try {
        candidateSuggestions = await computeFinalSuggestions(
          manager: manager,
          budget: budget,
          performance: performance,
          request: completionRequest,
          maxSuggestions: params.maxResults,
          notImportedSuggestions: notImportedSuggestions,
          useFilter: true,
        );
      } on AbortCompletion {
        return server.sendResponse(
          CompletionGetSuggestions2Result(
            completionRequest.replacementOffset,
            completionRequest.replacementLength,
            [],
            true,
          ).toResponse(request.id, clientUriConverter: server.uriConverter),
        );
      }
      // Keep track of whether the set of results was truncated (because
      // budget was exhausted).
      bool isIncomplete =
          (manager.notImportedSuggestions?.isIncomplete ?? false) ||
          manager.isTruncated;

      completionPerformance.transmittedSuggestionCount =
          candidateSuggestions.length;

      await performance.run('mapSuggestions', (performance) async {
        for (var candidate in candidateSuggestions) {
          var item = await candidateToCompletionSuggestion(
            candidate,
            completionRequest,
          );
          if (item != null) {
            suggestions.add(item);
          }
        }
      });

      performance.run('sendResponse', (_) {
        sendResult(
          CompletionGetSuggestions2Result(
            completionRequest.replacementOffset,
            completionRequest.replacementLength,
            suggestions,
            isIncomplete,
          ),
        );
      });
    });
  }

  void setNewRequest(DartCompletionRequest request) {
    _abortCurrentRequest();
    server.completionState.currentRequest = request;
  }

  /// Abort the current completion request, if any.
  void _abortCurrentRequest() {
    var currentRequest = server.completionState.currentRequest;
    if (currentRequest != null) {
      currentRequest.abort();
      server.completionState.currentRequest = null;
    }
  }
}
